package com.zeyu.framework.monitors.mongodb;

import com.beust.jcommander.internal.Lists;
import com.google.common.collect.Sets;
import com.mongodb.BasicDBObject;
import com.mongodb.MongoClient;
import com.mongodb.client.MongoDatabase;
import com.zeyu.framework.core.configuration.MongoDBConfiguration;
import com.zeyu.framework.monitors.Collector;
import com.zeyu.framework.monitors.mongodb.entity.CollectionStatus;
import com.zeyu.framework.monitors.mongodb.entity.DBStatus;
import com.zeyu.framework.monitors.mongodb.entity.MongoDBStatus;
import com.zeyu.framework.monitors.mongodb.service.MongoDBService;
import com.zeyu.framework.utils.StringUtils;
import org.apache.commons.collections4.IterableUtils;
import org.bson.BsonDocument;
import org.bson.BsonString;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 执行MongoDB的采集任务，将来可能会被任务调度使用，都是独立的方法
 * Created by zeyuphoenix on 16/8/26.
 */
@Component
public class MongoDBCollector implements Collector {

    // ================================================================
    // Constants
    // ================================================================

    /**
     * logger
     */
    private static final Logger logger = LoggerFactory.getLogger(MongoDBCollector.class);

    // ================================================================
    // Fields
    // ================================================================

    @Autowired
    private MongoClient mongoClient;
    @Autowired
    private MongoDBService mongoDBService;

    // 最后一次采集的mongodb状态
    private MongoDBStatus mongoDBStatus;

    // ================================================================
    // Constructors
    // ================================================================

    // ================================================================
    // Methods from/for super Interfaces or SuperClass
    // ================================================================

    @Override
    public void collect() {
        logger.info("MongoDB收集任务启动...");

        // 获取当前的状态
        mongoDBStatus = getCurrentMongoDBServerStatus();

        mongoDBStatus.setTime(new Date());

        // 保存信息到数据库
        mongoDBService.save(this.mongoDBStatus);
    }

    // ================================================================
    // Public or Protected Methods
    // ================================================================

    /**
     * 获取最后一次采集的mongodb状态
     */
    public MongoDBStatus getMongoDBStatus() {
        return this.mongoDBStatus;
    }

    /**
     * 获取当前MongoDB的状态
     *
     * @return 状态
     */
    public MongoDBStatus getCurrentMongoDBServerStatus() {

        MongoDBStatus mongoDBStatus = new MongoDBStatus();
        try {
            // 获取admin的db连接
            MongoDatabase db = mongoClient.getDatabase(MongoDBConfiguration.AUTHENTICATION_DATABASE_NAME);

            // 获取数据库状态
            Document commandResult = db.runCommand(new BasicDBObject("serverStatus", 1));
            logger.debug("MongoDB server status: ok");
            mongoDBStatus.setStatus(true);

            // 设置不变项目，不保存数据库
            String serverUsed = getValue(commandResult, "serverUsed", "127.0.0.1:27017");
            if (serverUsed != null) {
                // 截取
                String[] hostIps = StringUtils.split(serverUsed, ":");
                mongoDBStatus.setHost(hostIps[0]);
                if (hostIps.length > 1) {
                    mongoDBStatus.setPort(Integer.valueOf(hostIps[1].trim()));
                } else {
                    mongoDBStatus.setPort(27017);
                }
            } else {
                // 无法取得设置默认值
                mongoDBStatus.setHost("127.0.0.1");
                mongoDBStatus.setPort(27017);
            }

            // 获取版本、主机名
            mongoDBStatus.setVersion(getValue(commandResult, "version", "unKnown"));
            mongoDBStatus.setHostName(getValue(commandResult, "host", "unKnown"));
            // 获取进程、进程号
            mongoDBStatus.setPid(getValue(commandResult, "pid", 0));
            mongoDBStatus.setProcess(getValue(commandResult, "process", "mongod"));
            // 获取运行时间
            mongoDBStatus.setUptime(getValue(commandResult, "uptime", 0L));

            // 内存信息--详细请查看server status类
            Document mem = (Document) commandResult.get("mem");
            mongoDBStatus.setMemResident(getValue(mem, "resident", 0L));
            mongoDBStatus.setMemVirtual(getValue(mem, "virtual", 0L));
            mongoDBStatus.setMemMapped(getValue(mem, "mapped", 0L));
            mongoDBStatus.setMemMappedWithJournal(getValue(mem, "mappedWithJournal", 0L));

            // 连接数--详细请查看server status类
            Document connections = (Document) commandResult.get("connections");
            mongoDBStatus.setConnectionsAvailable(getValue(connections, "available", 0));
            mongoDBStatus.setConnectionsCurrent(getValue(connections, "current", 0));

            // 加载磁盘内容时发生页错误的次数
            Document extra_info = (Document) commandResult.get("extra_info");
            mongoDBStatus.setExtraInfoPageFaults(getValue(extra_info, "extra_info", 0L));

            // 网络--详细请查看server status类
            Document network = (Document) commandResult.get("network");
            mongoDBStatus.setNetworkBytesIn(getValue(network, "bytesIn", 0L));
            mongoDBStatus.setNetworkBytesOut(getValue(network, "bytesOut", 0L));
            mongoDBStatus.setNetworkNumRequests(getValue(network, "numRequests", 0L));

            // 操作数--详细请查看server status类
            Document opcounters = (Document) commandResult.get("opcounters");
            if (opcounters != null) {
                mongoDBStatus.setOpcountersInsert(getValue(opcounters, "insert", 0L));
                mongoDBStatus.setOpcountersUpdate(getValue(opcounters, "update", 0L));
                mongoDBStatus.setOpcountersQuery(getValue(opcounters, "query", 0L));
                mongoDBStatus.setOpcountersDelete(getValue(opcounters, "delete", 0L));
                mongoDBStatus.setOpcountersCommand(getValue(opcounters, "command", 0L));
            }

            // 索引次数--详细请查看server status类
            Document indexCounters = (Document) commandResult.get("indexCounters");
            if (indexCounters != null) {
                mongoDBStatus.setIndexCountersAccesses(getValue(indexCounters, "accesses", 0L));
                mongoDBStatus.setIndexCountersHits(getValue(indexCounters, "hits", 0L));
                mongoDBStatus.setIndexCountersMisses(getValue(indexCounters, "misses", 0L));
                mongoDBStatus.setIndexCountersResets(getValue(indexCounters, "resets", 0L));
            }

            mongoDBStatus.setTime(new Date());

        } catch (Exception e) {
            mongoDBStatus.setStatus(false);
            logger.error("获取mongodb状态失败: ", e);
        }

        return mongoDBStatus;
    }

    /**
     * 获取当前MongoDB的数据库列表
     *
     * @return 数据库列表
     */
    public List<DBStatus> getDbList() {
        // 返回数据库名称列表，后面会修改
        List<DBStatus> dbStatusList = Lists.newArrayList();
        try {

            // 获取目前的数据库列表
            List<String> dbNames = IterableUtils.toList(mongoClient.listDatabaseNames());
            logger.debug("database list is: " + dbNames);
            // 在MongoDB中，出于特殊目的（复制机制），保留性使用了local数据库。当使用认证机制时，对local数据库等同于认证admin数据库
            if (dbNames != null && !dbNames.isEmpty()) {
                for (String dbName : dbNames) {
                    // 排除local数据库
                    if (StringUtils.equalsIgnoreCase(MongoDBConfiguration.LOCAL_DATABASE_NAME, dbName)) {
                        continue;
                    }
                    dbStatusList.add(getDbStatusByName(dbName));
                }
            }
        } catch (Exception e) {
            logger.error("获取db数据库列表失败: ", e);
        }

        return dbStatusList;
    }

    /**
     * 根据数据库名称获取数据库的状态
     *
     * @param dbName
     *            db名称
     */
    public DBStatus getDbStatusByName(String dbName) {

        DBStatus dbStatus = new DBStatus();
        try {

            // 获取dbName的db连接
            MongoDatabase db = mongoClient.getDatabase(dbName);
            // 获取数据库状态
            Document commandResult = db.runCommand(new BasicDBObject("dbStats", 1));
            logger.debug("MongoDB db status: " + commandResult);

            // 设置属性
            dbStatus.setDbName(dbName);
            dbStatus.setStatus(true);

            // 连接属性
            dbStatus.setCollections(getValue(commandResult, "collections", 0L));
            dbStatus.setObjects(getValue(commandResult, "objects", 0L));
            // 数据大小
            dbStatus.setDataSize(getValue(commandResult, "dataSize", 0L));
            // 事件和索引
            dbStatus.setNumExtents(getValue(commandResult, "numExtents", 0L));
            dbStatus.setIndexes(getValue(commandResult, "indexes", 0L));
            // 文件大小
            dbStatus.setFileSize(getValue(commandResult, "fileSize", 0L));
        } catch (Exception e) {
            dbStatus.setStatus(false);
            logger.error("获取db状态失败: ", e);
        }
        return dbStatus;
    }

    /**
     * 获取当前MongoDB的数据库的Collection列表
     *
     * @return Collection列表
     */
    public List<CollectionStatus> getDbCollectionList(String dbName) {
        // 返回数据库Collection列表，后面会修改
        List<CollectionStatus> dbCollStatusList = Lists.newArrayList();
        try {
            // 获取dbName的db连接
            // 获取操作开始
            MongoDatabase db = mongoClient.getDatabase(dbName);
            // 获取目前的数据库Collection列表
            Set<String> collNames = Sets.newHashSet(db.listCollectionNames());
            logger.debug(dbName + " collections list is: " + collNames);
            // 根据各个collection名称获得状态
            if (!collNames.isEmpty()) {
                dbCollStatusList.addAll(collNames.stream()
                        .map(collName -> getCollStatusByName(dbName, collName))
                        .collect(Collectors.toList()));
            }
        } catch (Exception e) {
            logger.error("获取当前MongoDB的数据库的Collection列表失败: {}", e);
        }

        return dbCollStatusList;
    }

    /**
     * 根据collection获得它的状态
     *
     * @param collName
     *            collection名称
     * @return 状态
     */
    public CollectionStatus getCollStatusByName(String dbName, String collName) {

        CollectionStatus collectionStatus = new CollectionStatus();
        try {

            // 获取dbName的db连接
            MongoDatabase db = mongoClient.getDatabase(dbName);
            // 获取数据库collection状态
            Document commandResult = db.runCommand(new BsonDocument("collStats", new BsonString(collName)));
            logger.debug("MongoDB collection status: " + commandResult);

            collectionStatus.setCollName(collName);
            collectionStatus.setStatus(true);
            // 总条数
            collectionStatus.setCount(getValue(commandResult, "count", 0L));
            // 空间
            collectionStatus.setNs(getValue(commandResult, "ns", collName));
            // 事件和索引
            collectionStatus.setIndexes(getValue(commandResult, "indexes", 0L));
            collectionStatus.setNumExtents(getValue(commandResult, "numExtents", 0L));
            // 大小
            collectionStatus.setSize(getValue(commandResult, "size", 0L));
        } catch (Exception e) {
            collectionStatus.setStatus(false);
            logger.error("获取collections状态失败: ", e);
        }
        return collectionStatus;
    }


    /**
     * 释放内存，注意锁和数据丢失问题
     */
    public void releaseMemory() {
        //释放内存可能导致数据库异常,确定进行该操作?
        try {
            // 获取dbName的db连接
            MongoDatabase db = mongoClient.getDatabase(MongoDBConfiguration.AUTHENTICATION_DATABASE_NAME);

            // 允许释放命令
            Document commandResult = db.runCommand(new BasicDBObject("closeAllDatabases", 1));
            logger.info("close all databases，the status: {}", commandResult.toJson());
            commandResult = db.runCommand(new BasicDBObject("serverStatus", 1));
            logger.info("MongoDB release memory，the new server status: {}", commandResult);
        } catch (Exception e) {
            logger.error("释放内存失败: ", e);
        }
    }

    /**
     * 释放硬盘，注意锁和数据丢失问题
     *
     * MongoDB删除集合后磁盘空间不释放，只有用db.repairDatabase()去修复才能释放。
     *
     * 修复可能要花费很长的时间,在使用db.repairDatabase()去修复时一定要停掉读写，并且MongoDB要有备机才可以，
     * 不然千万不要随便使用db.repairDatabase()来修复数据库
     */
    public void releaseDisk() {
        //释放硬盘可能导致数据库异常,确定进行该操作?
        try {
            // 获取dbName的db连接
            MongoDatabase db = mongoClient.getDatabase(MongoDBConfiguration.AUTHENTICATION_DATABASE_NAME);

            // 允许释放命令
            Document commandResult = db.runCommand(new BasicDBObject("repairDatabase", 1));
            logger.info("repair databases，the status: {}", commandResult.toJson());
            commandResult = db.runCommand(new BasicDBObject("serverStatus", 1));
            logger.info("MongoDB release disk，the new server status: {}", commandResult);
        } catch (Exception e) {
            logger.error("释放硬盘失败: ", e);
        }
    }

    // ================================================================
    // Getter & Setter
    // ================================================================

    // ================================================================
    // Private Methods
    // ================================================================

    /**
     * 根据key获取值,没有使用默认值
     */
    private static String getValue(Document document, String key, String defaultValue) {

        Object val = document.get(key);
        if (val != null) {
            return val.toString();
        }

        return defaultValue;
    }

    /**
     * 根据key获取值,没有使用默认值
     */
    private static Long getValue(Document document, String key, Long defaultValue) {

        Object val = document.get(key);
        if (val != null) {
            return Double.valueOf(val.toString()).longValue();
        }

        return defaultValue;
    }

    /**
     * 根据key获取值,没有使用默认值
     */
    private static Integer getValue(Document document, String key, Integer defaultValue) {

        Object val = document.get(key);
        if (val != null) {
            return Double.valueOf(val.toString()).intValue();
        }

        return defaultValue;
    }

    // ================================================================
    // Inner or Anonymous Class
    // ================================================================

    // ================================================================
    // Test Methods
    // ================================================================

}
